TechNote - Window Focus

Thursday, June 4, 2026

7:21 AM

OneMore dialogs (search, etc.) may open without receiving keyboard focus — the focus stays on the OneNote page. This happens because Windows enforces strict foreground window security: SetForegroundWindow() silently fails unless the calling thread currently holds "foreground rights."

 

OneMore runs as a COM surrogate (dllhost.exe). Both trigger paths lose foreground rights before the dialog appears:

 

  • Ribbon path: SearchCmd is async Task — it returns the task to the ribbon framework immediately, and the dialog is created on a ThreadPool continuation thread that has no foreground rights.
  • Hotkey path: key.Action() is invoked from WndProc (which does have rights via WM_HOTKEY), but the async method suspends at the first await and continues on a ThreadPool thread — rights are lost.

 

The current Elevate() method uses a TopMost toggle trick and calls Select()/Focus(), but these don't actually transfer focus from another process. SetForegroundWindow is commented out because it fails silently without rights.

 

Solution: AttachThreadInput

The well-established technique is to temporarily attach the input queue of our dialog thread to the foreground thread (OneNote), which grants our thread the ability to call SetForegroundWindow successfully:

 

uint foregroundThread = Native.GetWindowThreadProcessId(foreground, out _);

uint currentThread = Native.GetCurrentThreadId();

Native.AttachThreadInput(foregroundThread, currentThread, true); // attach

Native.SetForegroundWindow(Handle);

Native.BringWindowToTop(Handle);

Native.AttachThreadInput(foregroundThread, currentThread, false); // detach immediately

 

The detach is immediate — the threads share the input queue only for the two API calls, which avoids any side effects on modal loops or message routing.

 

Files to Change

 

1. OneMore/Helpers/Native.cs

 

Add three P/Invoke declarations alongside the existing SetForegroundWindow / GetWindowThreadProcessId block:

[DllImport("kernel32.dll")]

public static extern uint GetCurrentThreadId();

[DllImport("user32.dll")]

[return: MarshalAs(UnmanagedType.Bool)]

public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);

[DllImport("user32.dll")]

[return: MarshalAs(UnmanagedType.Bool)]

public static extern bool BringWindowToTop(IntPtr hWnd);

 

2. OneMore/UI/MoreForm.cs — Elevate() method

Replace the comment-out //Native.SetForegroundWindow(Handle); and the TopMost-only approach with:

 

public void Elevate(bool keepTop = true)

{

if (DesignMode) return;

if (modeless) BringToFront();

// Temporarily share input queue with the foreground thread so

// SetForegroundWindow succeeds regardless of foreground rights.

var foreground = Native.GetForegroundWindow();

if (foreground != IntPtr.Zero && foreground != Handle)

{

uint foregroundThread = Native.GetWindowThreadProcessId(foreground, out _);

uint currentThread = Native.GetCurrentThreadId();

bool attached = foregroundThread != currentThread &&

Native.AttachThreadInput(foregroundThread, currentThread, true);

Native.SetForegroundWindow(Handle);

Native.BringWindowToTop(Handle);

if (attached)

{

Native.AttachThreadInput(foregroundThread, currentThread, false);

}

}

TopMost = false;

TopMost = true;

TopMost = keepTop;

if (!IsDisposed)

{

try { Select(); Focus(); }

catch { /* swallow disposed exception */ }

}

}

 

No changes to HotkeyManager, SearchCommand, WindowElevator, or any individual dialog.

 

 

#omwiki #omdeveloper #omtechnote

 

© 2020 Steven M Cohn. All rights reserved.

Please consider a sponsorship or one-time donation to support ongoing development

 

Created with OneNote.